package simulator;

import java.util.Stack;


/**
 * Clase que representa el procesador
 * es una clase singleton por lo que se usa getInstance() para obtener una 
 * instancia.
 * @author Michael Schmidt, Oscar Lopes, Andres Hidalgo
 */
public class Processor {
    
    /**
     * Singleton
     * SOLO EXISTE UN PROCESADOR
     */
    private static Processor instance = null;
    /**
     * La memoria que corresponde a este pocesador
     */
    private Ram memory;
    /**
     * Los registros del Procesador
     */
    protected String IR,AC,PC,IB,DB,PSW,SP;
    /**
     * Variables para interrupciones
     */
    protected boolean isInIt = false;
    protected boolean it;
    protected int itNr;
    /**
     * Banderas que se usan para averiguar el estado del PSW
     */
    private boolean negativeOperationResult = false;
    private boolean operationOverFlow = false;
    /**
     * Fin de las variables para saltarse el PSW
     */
    /**
     * Ciclos que faltan por esperar.
     */
    private int sleepCycles;
    /**
     * Variable que indica si el procesador llego al final
     * de la ejecucion
     */
     private boolean isAtEnd;
     
     private Stack<String> ibStack;
    /**
     * Constructor
     */
    private Processor(){
        IR = "0000";
        AC = "0000";
        PC = "0000";
        IB = "0008";
        DB = "0000";
        SP = "FFF";
        PSW = "00";
        sleepCycles = 0;
        isAtEnd = false;
        it = false;
        itNr = 0;
        
        memory = Ram.getInstance();
        ibStack = new Stack<String>();
    }
    
    /**
     * Metodo para obtener instancia del Procesador
     * @return la instancia
     */
    public static Processor getInstance(){
        if(instance == null){
            instance = new Processor();
            return instance;
        }else{
            return instance;
        }
    }
    
    /**
     * Metodo auxiliar para calcular la dirección real de una dirección relativa
     * @param Dirección Base
     * @param Direccion Relativa
     * @return Dirección Real
     */
    protected String calculateRealAddress(String baseAdress, String relativeAddress) throws AddressOverFlowException{
        int baseAsInt = Integer.valueOf(baseAdress,16);
        int relAsInt = Integer.valueOf(relativeAddress,16);
        String result = Integer.toHexString(baseAsInt+relAsInt);
        if(result.length()>3){
            throw new AddressOverFlowException();
            
        }
        return result;        
    }    
    /**
     * Metodo que libera las variables que representan el PSW
     * para que en el siguiente ciclo tengan valores neutrales.
     */
    protected void resetPswVars(){
        if(integerValue(AC)>=0){
            negativeOperationResult = false;
        }
        operationOverFlow = false;
    }
    /**
     * Metodo que evalua el PSW actualizando las Varibles,
     * negativeOperationResult
     * operationOverFlow
     * ...
     * 
     */
    protected void evalPsw(){
        if(PSW.equals("00")){
            return;
        }
        char[] psw = pswToArray();
        if(psw[7]=='1'){
            negativeOperationResult = true;
        }
        if(psw[6]=='1'){
            operationOverFlow = true;
        }        
        PSW = "00";//reset the PSW since all info is in Flags Now
    }
    /**
     * Metodo que transforma el PSW a un arreglo de 1os y 0ros
     * @return
     */
    protected char[] pswToArray(){
        String binPsw = Integer.toBinaryString(Integer.valueOf(PSW));
        if(binPsw.length()<8){
            for(int i=binPsw.length();i<8;i++){
                binPsw = 0+binPsw;
            }
        }        
        char[] result = new char[8];
        for(int i=0;i<8;i++){
            result[i] = binPsw.charAt(i);
        }        
        return result;
    }
    /**
     * Metodo que retorna el valor con signo de un numero Hexadecimal
     * @param hexString
     * @return
     */
    protected int integerValue(String hexString){
        String binaryNr = Integer.toBinaryString(Integer.valueOf(hexString,16));
        if(16 != binaryNr.length()){
            for(int i=binaryNr.length();i<16;i++){
                binaryNr = 0 + binaryNr;
            }
        }        
        if(binaryNr.charAt(0)=='0'){
            return Integer.valueOf(hexString,16);
        }else{
            return -1*Integer.valueOf(binaryNr.substring(1));
        }
    }
    /**
     * Metodo que cambia un numero con signo entero a su
     * correspondiente String Hexadecimal
     * @param Value
     * @return Hex String
     */
    protected String toHexString(int value) throws OperationOverflowException{
        String result;
        if(value<0){
            String tmpBinStr = Integer.toBinaryString(-1*value);
            if(tmpBinStr.length()<16){
                for(int i=tmpBinStr.length();i<16;i++){
                    if(i!=15){
                        tmpBinStr = 0+tmpBinStr;
                    }else{
                        tmpBinStr = 1+tmpBinStr;
                    }
                }
            }            
            result = Integer.toHexString(Integer.valueOf(tmpBinStr,2));
        }else{
            result = Integer.toHexString(value);
        }
        if(result.length()>4){
            throw new OperationOverflowException();
        }
        return result;
    }
    /**
     * Metodo para insertar un valor a la pila
     * @param Valor a ingresar
     */
    protected void pushOnStack(String value) throws StackOverflowException{
        int intSP = Integer.valueOf(SP,16);
        //System.out.println("intSp = "+intSP);
        if(intSP>0){
            memory.setStackField(SP, value);
            intSP--;
            SP = Integer.toHexString(intSP);
        }else{
            throw new StackOverflowException();
        }
    }
    /**
     * Metodo que retira el primer elemento de la pila y lo retorna.
     * @return Primer elemento de la Pila
     */
    protected String popOfStack() throws StackUnderFlowException{
        int intSp = Integer.valueOf(SP,16);
        intSp++;
        if(intSp>4095){
            throw new StackUnderFlowException();
        }
        SP = Integer.toHexString(intSp);
        return memory.getDataOfStack(SP);
    }
    /**
     * Metodo que trae la siguiente instruccion al registro IR
     */
    public void fetch() throws AddressOverFlowException{
       //System.out.println("Processor at fetch time = \n"+ this);
       String address = calculateRealAddress(IB,PC);
       //System.out.println(address);
       IR = memory.getInstruction(address); 
       //System.out.println("IR after fetch = "+ IR);
    }
    /**
     * Metodo que incrementa el PC en uno
     */
    protected void incrementPc() throws PcOverFlowException{
        int intPc = Integer.valueOf(PC,16);
        intPc++;
        String newPc = Integer.toHexString(intPc);
        if(newPc.length()>4){
            throw new PcOverFlowException();
        }
        PC = newPc;
        
    }
    /**
     * Metodo que reacciona ante una interrupcion, guarda el PC en el Stack y salta
     * al codigo de la interrupcion
     */
    protected void handelInterrupt() throws StackOverflowException, AddressOverFlowException{        
        if(it){
            it = false;
            pushOnStack(PC);
            pushOnStack(IB);
            pushOnStack(DB);
            PC = "0000";           
            IB = memory.getItAddress(itNr);
            DB = "0010";
            fetch();            
            //System.out.println("!!!!!!!!!!\n"+this+"!!!!!!!!!!!!\n");
        }
    }
    protected void insertInterrupt(int itNr) throws UnsuportedItException{
        if(itNr<0||itNr>7){
            throw new UnsuportedItException();
        }else{
            this.itNr = itNr;
            this.it = true;            
        }
    }
    /**
     * Metodo que ejecuta la instruccion en el IR
     */
    public void execute() throws PcOverFlowException, AddressOverFlowException, StackOverflowException, StackUnderFlowException, OpperationNotExistsException, ProgrammEndedException, OutOfProcessesException{
        if(sleepCycles > 0 ){//estamos durmiendo?
            sleepCycles--;
            IR = "0000";
        }
        handelInterrupt();//Si hay una interrupcion
        
        
        char instructionCode = IR.charAt(0);
        String address = IR.substring(1);
        switch (instructionCode)
            {
                case '0':
                    this.nop();
                    incrementPc();                    
                    break;
		case '1':
                    this.load(address);                    
                    incrementPc();                    
                    break;
		case '2':
                    this.move(address);
                    incrementPc();                    
                    break;
                case '3':
                    jnn(address);                    
                    break;
                case '4':
                    try{
                        add(address);
                    }catch(OperationOverflowException e){
                        operationOverFlow = true;
                    }catch(NegativeOpperationResultException e){
                        negativeOperationResult = true;
                    }
                    incrementPc();
                    break;
                case '5':
                    try{
                        this.csgn();
                    }catch(OperationOverflowException e){
                        operationOverFlow = true;
                    }catch(NegativeOpperationResultException e){
                        negativeOperationResult = true;
                    }
                    incrementPc();
                    break;
                case '8':
                    this.call(address);
                    break;
                case '9':
                    this.ret();
                    break;
                case 'A':
                    this.sleep(address);
                    incrementPc();
                    break;
                case 'B':
                    this.push();
                    incrementPc();
                    break;
                case 'C':
                    this.pop();
                    incrementPc();
                    break;
                case 'D':
                    this.rit();
                    break;
                case 'E':
                    this.callj();
                    incrementPc();
                    break;
                case 'F':
                    this.end();
                    break;
                default:
                        throw new OpperationNotExistsException();
            }
        resetPswVars();
        
    }
    /**
     * Operacion vacia, no hace nada
     */
    protected void nop(){        
    }    
    /**
     * Loads the data from the memory cell referenced by the address
     * parameter
     * @param Relative Address
     */
    protected void load(String relativeAddress) throws AddressOverFlowException{
        String realAdress = calculateRealAddress(DB, relativeAddress);
        AC = memory.getData(realAdress);
    }
    /**
     * Moves the value in the AC to the specified address
     * @param Relative Address
     */
    protected void move(String relativeAddress) throws AddressOverFlowException{
        String realAdress = calculateRealAddress(DB, relativeAddress);
        memory.setDataField(realAdress, AC);        
    }
    /**
     * Saltar a direccion !!relativa!! si la operacion anterior no dio un 
     * resultado negativo
     * @param Jump Address
     */
    protected void jnn(String relativeAddress) throws PcOverFlowException{
        if(!negativeOperationResult){
            PC = relativeAddress;
        }
        else{
            incrementPc();
        }
    }
    /**
     * Sumar el contenido de relativeAdress a AC y guardar resultado
     * en AC
     * @param Relative Address
     */
    protected void add(String relativeAddress) throws AddressOverFlowException, OperationOverflowException, NegativeOpperationResultException{
        String realAdress = calculateRealAddress(DB, relativeAddress);
        String temp = memory.getData(realAdress);
        int acValue = integerValue(AC);
        //System.out.println("acValue "+acValue);
        int toAdd = integerValue(temp);
        int intResult = acValue + toAdd;
        //System.out.println(intResult);
        AC = toHexString(intResult);
        if(intResult<0){
            throw new NegativeOpperationResultException();
        }
    }
    /**
     * Cambia el signo de lo que esta guardado en AC
     */
    protected void csgn() throws OperationOverflowException, NegativeOpperationResultException{
        int intAc = integerValue(AC);
        intAc *= -1;
        AC = toHexString(intAc);
        if(intAc<0){
            throw new NegativeOpperationResultException();
        }
    }
    /**
     * Saltar a la direccion address guardando el PC en la pila
     * @param address
     */
    protected void call(String relativeAddress) throws StackOverflowException{
        pushOnStack(PC);
        PC = relativeAddress;
    }
    /**
     * Retornar a la ejecución antes de saltar con call
     * @throws StackUnderFlowException
     */
    protected void ret() throws StackUnderFlowException{
        PC = popOfStack();        
    }
    /**
     * Metodo que para la ejecucion por la cantidad de ciclos que
     * aparecen en la casilla de memoria
     * @param Relative Address
     */
    protected void sleep(String relativeAddress) throws AddressOverFlowException{
        String realAdress = calculateRealAddress(DB, relativeAddress);
        String cycles = memory.getData(realAdress);
        sleepCycles = Integer.valueOf(cycles,16);
    }
    /**
     * Metodo que guarda el AC en la Pila
     */
    protected void push() throws StackOverflowException{
        pushOnStack(AC);
    }
    /**
     * Metodo que elimina el primer elemento de la pila,
     * guradando su valor en AC
     */
    protected void pop() throws StackUnderFlowException{
        AC = popOfStack();
    }
    /**
     * retornar de una interrupcion
     * @throws simulator.StackUnderFlowException
     */
    protected void rit() throws StackUnderFlowException{
        DB = popOfStack();
        IB = popOfStack();
        PC = popOfStack();       
    }
    /**
     * Metodo que finaliza la ejecución del procesador
     */
    protected void end() throws ProgrammEndedException{
        throw new ProgrammEndedException();
    }
    
    protected void callj() throws OutOfProcessesException{
        OperatingSystem.getInstance().loadNextProcess();
    }
    /**
     * Metodo auxiliar para el sistema operativo
     * retorna el nuevo valor del SP despues de hacer un 
     * push con el valor pasado como segundo parametro.
     * @param SP
     * @param value
     * @return
     */
    public String pushOnStack(String SP,String value){
        int intSP = Integer.valueOf(SP, 16);
        memory.setDataField(intSP, value);
        intSP--;
        return Integer.toHexString(intSP);
    }
    
    /**
     * para imprimir el estado del Procesador
     */
    public String toString(){
        String result = "PC "+PC;
        result += " AC " +AC; 
        result += "\nIR "+IR; 
        result += "\nIB "+IB;
        result += " DB "+DB;
        result += "\nSP "+SP;
        result += "\nPSW "+PSW;
        return result;
    }
    
}

